package org.hackystat.devcathlon.engine.event.impl;

import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;
import javax.xml.datatype.XMLGregorianCalendar;
import org.hackystat.devcathlon.engine.event.Event;
import org.hackystat.devcathlon.engine.event.EventException;
import org.hackystat.devcathlon.engine.event.EventScore;
import org.hackystat.devcathlon.engine.match.Match;
import org.hackystat.devcathlon.engine.team.Team;
import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.sensorbase.client.SensorBaseClientException;
import org.hackystat.sensorbase.resource.projects.jaxb.Project;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataIndex;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataRef;
import org.hackystat.utilities.logger.HackystatLogger;
import org.hackystat.utilities.tstamp.Tstamp;

/**
 * Deduct points if developer has not committed code within 120 minutes of development time.
 * <p>
 * Requires the following properties in the configuration.xml file for this event:
 * <ul>
 * <li>WakeupInterval: How frequently this event is invoked, and how far back it looks for data.
 * <li>BaseScoreWeight: An integer multiplied by the base score to get the base points.
 * <li>WakeupReferences: The number of dev events that occurred within a hour.
 * <li>PenaltyPoints: A point deduction for each violation of WakeupReferences.
 * <li>CommitScore: A given score for each commit, done within the DevTime, of an edited file .
 * </ul>
 * <p>
 * Specification:
 * <ul>
 * <li>Checks all DevEvent sensor data since the last wakeup interval for the DevEvent data
 * instance.
 * <li>Retrieves the DevEvent associated with this project. A running count is kept of each DevEvent
 * occurrence within the wakeupInterval.
 * <li>If a commit has been committed within DevTime, then return zero points.
 * <li>Base Score: 0 if commit has occurred within DevTime. Otherwise base score is -1 or less
 * depending on the increment of PenaltyTime.
 * <li>Bonus Score: None needed. Don't want to hit developers too hard for doing work.
 * </ul>
 * 
 * @author Daniel Arakaki, Scheller Sanchez
 */
public class CommitOften extends Event {

  /** Support serialization. */
  private static final long serialVersionUID = 1L;

  /** A name for this event. */
  public static final String eventName = "Commit Often";

  /** A description for this event. */
  public static final String description = "Penalize developers for working on new code too long "
      + "without committing it and thus making it available for others to see and integrate.";

  /** The property key for the wakeup reference time. */
  public static final String wakeupReferencesKey = "WakeupReferences";

  /** The actual wakeupReferences time. */
  private int wakeupReferences;

  /** Property key for base score. */
  public static final String baseScoreKey = "BaseScore";

  /** Actual base score. */
  private int baseScore;

  /** Property key for penalty points. */
  public static final String penaltyPointsKey = "PenaltyPoints";

  /** Actual penalty points. */
  private int penaltyPoints;

  /** Table for file references. */
  private Hashtable<String, Integer> wakeupReferencesTable;

  /** A set of file names. */
  private Set<String> fileSet;

  /** A count of wakeReferences. */
  private int referenceCount;

  /**
   * Create a new event for CommitOften. Check for all required properties and initialize.
   * 
   * @throws EventException If the configuration cannot be created.
   */
  public CommitOften() throws EventException {
    super(eventName, description);
    try {
      this.wakeupReferences = Integer.parseInt(configManager.getProperty(eventName,
          wakeupReferencesKey));
      this.baseScore = Integer.parseInt(configManager.getProperty(eventName, baseScoreKey));
      this.penaltyPoints = Integer.parseInt(configManager.getProperty(eventName, penaltyPointsKey));
    }
    catch (Exception e) {
      throw new EventException("Error configuring properties", e);
    }

    this.wakeupReferencesTable = new Hashtable<String, Integer>();
    this.fileSet = new HashSet<String>();
  }

  /**
   * Returns the total number of points for this event. The total is the base score multiplied the
   * base score weight.
   * 
   * @param score The event score.
   * @return The total points for this event given the current weights.
   */
  @Override
  public int computePoints(EventScore score) {
    return (score.getBaseScore() * this.baseScoreWeight);
  }

  /**
   * Returns an event score for Commit Often.
   * 
   * @param match The match of interest.
   * @param team The team being examined.
   * @param timestamp The time when this event is being run.
   * @return The EventScore.
   * @throws EventException If an error occurs during the event process.
   */
  @Override
  public EventScore computeScore(Match match, Team team, XMLGregorianCalendar timestamp)
      throws EventException {
    EventScore score = new EventScore(timestamp, match, team);
    Project project = match.getProject(team);

    SensorBaseClient sbClient = match.getSensorBaseClient(team);

    String projectName = project.getName();
    String owner = project.getOwner();

    HackystatLogger.getLogger("org.hackystat.devcathlon", "devcathlon").info(
        String.format("Getting commit data for match %s and team %s", match.getName(), team
            .getName()));

    XMLGregorianCalendar endTime = Tstamp.makeTimestamp();
    XMLGregorianCalendar startTime = Tstamp.incrementMinutes(endTime, wakeupInterval * -1);

    try {
      SensorDataIndex sdIndex = sbClient.getProjectSensorData(owner, projectName, startTime,
          endTime);

      this.extractEvent(sdIndex, sbClient);

      HackystatLogger.getLogger("org.hackystat.devcathlon", "devcathlon").info(
          String.format("File and Count: %s, %s", fileSet.iterator().next(),
              this.wakeupReferencesTable.get(fileSet.iterator().next())));

    }
    catch (Exception e) {
      throw new EventException(String.format("Error getting commit data for match %s and team %s",
          match.getName(), team.getName()), e);
    }

    score.setBaseScore(this.computeBaseScore(score));
    return score;
  }

  /**
   * Return the computed score.
   * 
   * @param score The EventScore.
   * @return The computed score.
   */
  private int computeBaseScore(EventScore score) {

    if ((referenceCount >= 10) && (referenceCount < 20)) {
      score.setExplanation("No commit made within 10 DevEvents.");
      return this.penaltyPoints;
    }
    else if (referenceCount >= 20) {
      score.setExplanation("No commit made in 20 DevEvents.");
      return this.penaltyPoints * 2;
    }
    else {
      score.setExplanation("There has been a commit since last wakeup.");
      return this.baseScore;
    }
  }

  /**
   * Extract Events and track each reference for the interval.
   * 
   * @param sensorIndex A SensorDataIndex instance.
   * @param sbClient A SensorBaseClient instance.
   * @throws SensorBaseClientException Problems with accessing sensor data.
   */
  private void extractEvent(SensorDataIndex sensorIndex, SensorBaseClient sbClient)
      throws SensorBaseClientException {
    for (SensorDataRef dataRef : sensorIndex.getSensorDataRef()) {

      SensorData data = sbClient.getSensorData(dataRef);
      String fileName = this.getFileName(data);
      if (data.getSensorDataType().equals("Commit")) {
        this.wakeupReferencesTable.put(fileName, wakeupReferences);
      }
      else if (data.getSensorDataType().equals("DevEvent")) {
        this.fileSet.add(fileName);
        this.wakeupReferencesTable.put(fileName, ++wakeupReferences);
        this.referenceCount = this.wakeupReferencesTable.get(fileName);
      }
    }
  }

  /**
   * Return the parsed resource.
   * 
   * @param sensorRef A reference to the sensor data.
   * @return The file's parsed name.
   */
  private String getFileName(SensorData sensorRef) {
    String fileName = sensorRef.getResource();
    int fileNameIndex = fileName.lastIndexOf('/');
    String fileNameBuffer = fileName.substring(fileNameIndex + 1);
    return fileNameBuffer;
  }

}
